<?php

namespace app\service\modbus;

use Exception;
use ModbusTcpClient\Composer\Read\ReadRegistersBuilder;
use ModbusTcpClient\Composer\Request;
use ModbusTcpClient\Network\BinaryStreamConnection;
use ModbusTcpClient\Packet\ModbusFunction\WriteMultipleRegistersRequest;
use ModbusTcpClient\Packet\RtuConverter;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Packet;
use ModbusTcpClient\Utils\Types;

class ModbusRtuOrTcpService
{
    protected BinaryStreamConnection $connection;
    protected string $host;
    protected int $port;

    public function __construct(string $host = '127.0.0.1', int $port = 501, $timeout = 0.5)
    {
        Endian::$defaultEndian = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $this->connection = BinaryStreamConnection::getBuilder()
            ->setPort($port)
            ->setHost($host)
            ->setConnectTimeoutSec(1.5)
            ->setWriteTimeoutSec($timeout)
            ->setReadTimeoutSec(0.3)
            ->setIsCompleteCallback(function ($binaryData, $streamIndex) {
                return Packet::isCompleteLengthRTU($binaryData);
            })
            ->build();
        $this->host = $host;
        $this->port = $port;
    }

    /**
     * 判断modbus寄存器是否在线
     * @return string
     */
    public function isOnline(): string
    {
        $connection = $this->connection;
        try {
            $connection->connect();
            $is_online = "online";
        } catch (\Exception $exception) {
            $is_online = "offline";
        } finally {
            $connection->close();
        }
        return $is_online;
    }

    /**
     * 获取modbus-tcp-or-rtu的值
     * @param int $address
     * @param string $name
     * @param string $get_type
     * @param int $bit
     * @param int $unitID
     * @return mixed|string|true
     */
    public function getModbusTcpOrRtuValue(int $address, string $name, string $get_type = 'int16', int $bit = -99, int $unitID = 1): mixed
    {
        $server = ReadRegistersBuilder::newReadHoldingRegisters('no_address', $unitID);
        switch ($get_type) {
            case "bit":
                if ($bit < 0 || $bit > 15) {
                    return "bit位有误";
                }
                $server = $server->bit($address, $bit, $name);
                break;
            case "int16":
                $server = $server->int16($address, $name);
                break;
            case "uint16":
                $server = $server->uint16($address, $name);
                break;
            case "int32":
                $server = $server->int32($address, $name);
                break;
            case "uint32":
                $server = $server->uint32($address, $name);
                break;
            case "uint64":
                $server = $server->uint64($address, $name);
                break;
            case "float":
                $server = $server->float($address, $name);
                break;
            case "string":
                $server = $server->string($address, $name);
                break;
            default:
                $server = $server->byte($address, $bit,$name);
                break;
        }
        $requests = $server->build();
        try {
            foreach ($requests as $request) {
                return $this->getResult($request);
            }
            return true;
        } catch (Exception $exception) {
            return $exception->getMessage();
        } finally {
            $this->connection->close();
        }
    }

    /**
     * 设置modbus-tcp-or-rtu的值
     * @param int $address
     * @param int $value
     * @param string $get_type
     * @param int $bit
     * @param int $unitID
     * @return array|string
     */
    public function setModbusTcpOrRtuValue(int $address, int $value, string $get_type = 'int16', int $bit = -99, int $unitID = 1): array|string
    {
        try {
            if ($bit >= 0 && $bit <= 16) {
                return $this->setBitReadValue($address, $bit, $value, $unitID);
            }
            return $this->setModbusWriteMessage($address, $value, $get_type, $unitID);
        } catch (Exception $exception) {
            return $exception->getMessage();
        } finally {
            $this->connection->close();
        }
    }

    /**
     * 设置bit位的值
     * @param int $address
     * @param int $bit
     * @param int $value
     * @param int $unitID
     * @return array|string
     */
    public function setBitReadValue(int $address = 0, int $bit = 0, int $value = 0, int $unitID = 1): array|string
    {
        if ($bit < 0 || $bit > 15) {
            return "bit位有误";
        }
        if (!in_array($value, [0, 1])) {
            return "bit值只有0,1";
        }
        $data_value = $this->getAddressList($address, 1, $unitID);
        $old_value = $data_value[1];
        if ($bit == 0) {
            $pos = -1;
        } else {
            $pos = "-" . ($bit + 1);
        }
        try {
            $string = decbin($old_value);
            $string = str_pad($string, 16, "0", STR_PAD_LEFT);
            $new_value = bindec(substr_replace($string, $value, $pos, 1));
            return $this->setModbusWriteMessage($address, $new_value, "uint16", $unitID);
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 获取地址中的bit位的值
     * @param int $address
     * @param int $bit
     * @param string $name
     * @param int $unitID
     * @return string|void
     */
    public function getBitReadValue(int $address = 0, int $bit = 0,string $name="value", int $unitID = 1)
    {
        $requests = ReadRegistersBuilder::newReadHoldingRegisters('no_address', $unitID)
            ->bit($address, $bit,$name)
            ->build();
        try {
            $result="";
            foreach ($requests as $request) {
                $result = $this->getResult($request);
            }
            return $result;
        } catch (Exception $exception) {
            return $exception->getMessage();
        } finally {
            $this->connection->close();
        }
    }
    /**
     * 获取地址中的byte位的值
     * @param int $address
     * @param int $bit
     * @param int $unitID
     * @return string|void
     */
    public function getByteReadValue(int $address = 0, int $bit = 0, int $unitID = 1)
    {
        $byte = true;
        if ($bit==0){
            $byte = false;
        }
        $requests = ReadRegistersBuilder::newReadHoldingRegisters('no_address', $unitID)
            ->byte($address, $byte,'value')
            ->build();
        try {
            $result="";
            foreach ($requests as $request) {
                $result = $this->getResult($request);
            }
            return $result;
        } catch (Exception $exception) {
            return $exception->getMessage();
        } finally {
            $this->connection->close();
        }
    }

    /**
     * 获取开始值范围内的寄存器的值
     * @param int $address
     * @param int $number
     * @param string $get_type
     * @param int $unitID
     * @return array|string
     */
    public function getAddressList(int $address, int $number,string $get_type="int16", int $unitID = 1): array|string
    {
        try {
            $server = ReadRegistersBuilder::newReadHoldingRegisters("no_address", $unitID);
            for ($i = $address; $i <= ($address + $number); $i++) {
                $server = match ($get_type) {
                    "uint16" => $server->uint16($address, $i),
                    "int32" => $server->int32($address, $i),
                    "uint32" => $server->uint32($address, $i),
                    "uint64" => $server->uint64($address, $i),
                    "float" => $server->float($address, $i),
                    "string" => $server->string($address, $i),
                    default => $server->int16($address, $i),
                };
            }
            $data = [];
            $requests = $server->build();
            foreach ($requests as $request) {
                $data = $this->getResult($request);
            }
            return $data;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        }
    }

    /**
     * 写入modbus-tcp-rtu写入参数的请求和响应报文
     * @param int $address
     * @param int $value
     * @param string $dataType
     * @param int $unitID
     * @return array|string
     */
    public function setModbusWriteMessage(int $address, int $value, string $dataType = 'uint16', int $unitID = 1): array|string
    {
        $endianess = Endian::BIG_ENDIAN_LOW_WORD_FIRST;
        $error = "";
        $v = "";
        switch ($dataType) {
            case 'uint8':
                if ($value < 0 || $value > 255) {
                    $error = "valid range of uint8 is 0 to 255";
                } else {
                    $v = Types::toByte($value);
                    if ($endianess & Endian::BIG_ENDIAN) {
                        $v = "\x00" . $v;
                    } else {
                        $v = $v . "\x00";
                    }
                }
                break;
            case 'int8':
                if ($value < -128 || $value > 127) {
                    $error = "valid range of int8 is -128 to 127";
                } else {
                    $v = Types::toByte($value);
                }
                if ($endianess & Endian::BIG_ENDIAN) {
                    $v = "\x00" . $v;
                } else {
                    $v = $v . "\x00";
                }
                break;
            case 'uint16':
                if ($value < 0 || $value > 65535) {
                    $error = "valid range of uint16 is 0 to 65535";
                } else {
                    $v = Types::toUint16($value);
                }
                break;
            case 'int16':
                if ($value < -32768 || $value > 32767) {
                    $error = "valid range of uint16 is -32768 to 32767";
                } else {
                    $v = Types::toInt16($value);
                }
                break;
            case 'uint32':
                if ($value < 0 || $value > 4294967295) {
                    $error = "valid range of uint32 is 0 to 4294967295";
                } else {
                    $v = Types::toUint32($value);
                }
                break;
            case 'int32':
                if ($value < -2147483648 || $value > 2147483647) {
                    $error = "valid range of int32 is -2147483648 to 2147483647";
                } else {
                    $v = Types::toInt32($value);
                }
                break;
            case 'float32':
                if ($value < -3.4e+38 || $value > 3.4e+38) {
                    $error = "valid range of float32 is -3.4e+38 to 3.4e+38";
                } else {
                    $v = Types::toReal($value);
                }
                break;
            case 'uint64':
                $value = (int)$value;
                if ($value < 0 || $value > 9223372036854775807) {
                    $error = "valid range of uint64 is 0 to 9223372036854775807";
                } else {
                    $v = Types::toUint64($value);
                }
                break;
            case 'int64':
                if ($value < 0 || $value > 9223372036854775807) {
                    $error = "valid range of int64 is -9223372036854775808 to 9223372036854775807";
                } else {
                    $v = Types::toInt64($value);
                }
                break;
            case 'float64':
                $value = (double)$value;
                if ($value < 1.7E-308 || $value > 1.7E+308) {
                    $error = "valid range of float64 is 1.7E-308 to 1.7E+308";
                } else {
                    $v = Types::toDouble($value);
                }
                break;
            default:
                $error = "invalid data type";
                break;
        }
        if ($error) {
            return $error;
        }
        try {
            $packet = new WriteMultipleRegistersRequest($address, [$v], $unitID);
            $packet = RtuConverter::toRtu($packet);
            $binaryData = $this->connection->connect()->sendAndReceive($packet);
            $message['request_message'] = unpack('H*', $packet)[1];
            $message['response_message'] = unpack('H*', $binaryData)[1];
            $message['address'] = $address;
            $message['value'] = $value;
            return $message;
        } catch (\Exception $exception) {
            return $exception->getMessage();
        } finally {
            $this->connection->close();
        }
    }

    /**
     * @param Request $request
     * @return mixed
     * @throws Exception
     */
    public function getResult(Request $request): mixed
    {
        $rtuPacket = RtuConverter::toRtu($request->getRequest());
        $binaryData = $this->connection->connect()->sendAndReceive($rtuPacket);
        $tcpResponsePacket = RtuConverter::fromRtu($binaryData);
        $result = $request->parse($tcpResponsePacket);
        $result['request_message'] = $request->getRequest()->toHex();
        $result['response_message'] = unpack('H*', $binaryData)[1];
        return $result;
    }

}